﻿#region MIT Licence
/*
 * Copyright (C) 2014 PdfCreative
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom 
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Text;

using PdfCreative.Pdf.Writer;
using PdfCreative.Pdf.Writer.Fonts;
using PdfCreative.Pdf.Writer.Utilities;

namespace PdfCreative.Pdf.Graphics.TextRendering
{
    /// <summary>
    /// The TextFlowEngine class encapsulates the functionality of flowing text inside a bounding box.
    /// It has method to start a new flow, append sections to a flow, and finalise the flow.
    /// The TextFlowEngine reports whenever it has flowed text onto a new line through its Flow event.
    /// Adding a section to the engine may result in 0, 1 or multiple events being raised, one for
    /// each line that is completed.
    /// </summary>
    internal class TextFlowEngine
    {
        // Private fields
        // ==============
        #region Engine state
        private Dictionary<string, FontInfo> _fontDirectory;
        #endregion

        #region Flow state
        private PdfRectangle _bounds;
        private float _cumulativeLineHeight;
        private float _currentFontSize;
        private TextFlow _flow;
        private StringFormat _format;
        private float _largestFontSize;
        private float _lineHeight;
        private float _lineWidth;
        private List<Token> _pendingText;
        private List<Token> _pendingWhitespace;
        private ParseState _state = ParseState.Whitespace;
        private float _tokenWidth;
        private float _whitespaceWidth;
        #endregion



        // Public events
        // =============
        #region Public events
        public event TextFlowEventHandler Flow;
        #endregion



        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Starts a new text flow session. This clears the current operation.
        /// </summary>
        /// <param name="bounds">The boundaries to flow text in</param>
        /// <param name="format">The text formatting style</param>
        public void Start(PdfRectangle bounds, StringFormat format)
        {
            // Store the parameters of this flow
            _bounds = bounds;
            _format = format;

            // Reset state
            _cumulativeLineHeight = 0f;
            _currentFontSize = 0f;
            _flow = new TextFlow();
            _largestFontSize = 0f;
            _lineHeight = 0f;
            _lineWidth = 0f;
            _pendingText.Clear();
            _pendingWhitespace.Clear();
            _state = ParseState.Text;
            _tokenWidth = 0f;
            _whitespaceWidth = 0f;
        } // Start()

        /// <summary>
        /// Flows a new piece of inline text inside the bounds.
        /// </summary>
        /// <param name="inline">The inline text to flow</param>
        public void AddSection(InlineText inline)
        { 
            // load the font data and set up the text rendering data structure.
            FontInfo fontInfo = GetFontInfo(inline.Font);
            if (null == fontInfo)
                throw new InvalidOperationException(String.Format(
                    "No font information for the specified font '{0}' could be found.", inline.Font.TypeName));

            // Start parsing characters if there is any space left in the bounding rectangle.
            if(_cumulativeLineHeight <= _bounds.Height)
                ParseCharacters(inline, fontInfo);

        } // AddSection()

        /// <summary>
        /// Finalises the current operation. This outputs the final pending whitespace and text.
        /// </summary>
        /// <returns></returns>
        public void Finalise()
        {
            // We output pending whitespace tokens and pending text tokens, if they fit. It is possible that this will output a new line.
            // For more details on text flow, see the ParseCharacters() method.
            if (_lineWidth + _whitespaceWidth + _tokenWidth > _bounds.Width)
            {
                AbandonPendingWhitespace();
                NextLine();
            }
            else OutputPendingWhitespace();

            // If there's still space, output the final line.
            if (_cumulativeLineHeight <= _bounds.Height)
            {
                _lineWidth += _whitespaceWidth + _tokenWidth;
                OutputPendingText();

                // Raise an event for the final line, if it isn't empty.
                OnFlow(false);
            }


        } // Finalise()
        #endregion


        // Private implementation
        // ======================
        #region Private implementation
        /// <summary>
        /// Abandons currently pending whitespace tokens.
        /// </summary>
        private void AbandonPendingWhitespace()
        {
            _whitespaceWidth = 0f;
            _pendingWhitespace.Clear();
        }

        /// <summary>
        /// Retrieves a font information structure for the specified font.
        /// </summary>
        /// <param name="font">The font</param>
        /// <returns>A TrueTypeFontInfo instance that contains information on the font.</returns>
        private FontInfo GetFontInfo(Font font)
        {
            System.Diagnostics.Debug.Assert(null != font);

            FontInfo fontInfo = null;
            string typeName = font.TypeName.ToLower();

            // First we look the font up in the font directory. If it's in there we use the font information from
            // the directory. Otherwise, we need to find the font definition and load it.
            if (_fontDirectory.ContainsKey(typeName))
                fontInfo = _fontDirectory[typeName];
            else
            {
                // Retrieve the font's location and load it.
                FontLocation location = FontUtilities.LocateFont(font.TypeName);

                if (null != location)
                {
                    switch (location.LocationType)
                    {
                        case FontLocationType.TrueTypeFont:
                            fontInfo = new TrueTypeFontInfo();
                            fontInfo.Parse(location.MetricsPath);
                            break;

                        case FontLocationType.Type1Font:
                            fontInfo = new Type1FontInfo();
                            fontInfo.Parse(location.MetricsPath);
                            break;
                    }
                }

                // Store the new information structure in the directory.
                if (null != fontInfo)
                    _fontDirectory[typeName] = fontInfo;
            }

            return fontInfo;
        } // GetFontInfo()

        /// <summary>
        /// Moves to the next line in the output.
        /// This resets the line width, and updates the cumulative height of the text so far.
        /// The new line starts off with a font size that is that of the currently active font.
        /// Note that a new line is not output if it is empty - usethe NextLine(bool) overload
        /// to force an empty line to be output.
        /// </summary>
        private void NextLine()
        {
            NextLine(false);
        }

        /// <summary>
        /// Moves to the next line in the output.
        /// This resets the line width, and updates the cumulative height of the text so far.
        /// The new line starts off with a font size that is that of the currently active font.
        /// Optionally, the new line can be forced. In that case, a new line is output even if it
        /// is empty.
        /// </summary>
        /// <param name="force">Forces output of the current line, even if it is empty.</param>
        private void NextLine(bool force)
        {
            // Raise an event for the current line.
            OnFlow(force);

            _flow = new TextFlow();
            _lineHeight = _format.LineSpacing + _currentFontSize;
            _cumulativeLineHeight += _lineHeight;
            _lineWidth = 0;
        }

        /// <summary>
        /// Raises the Flow event.
        /// </summary>
        /// <param name="force">Forces output of the current line, even if it is empty.</param>
        private void OnFlow(bool force)
        {
            _flow.Height = _lineHeight;
            _flow.Width = _lineWidth;

            if (null != Flow && (true==force || _flow.Tokens.Count>0)) Flow(_flow);
        }

        /// <summary>
        /// Outputs currently pending whitespace tokens.
        /// </summary>
        private void OutputPendingWhitespace()
        {
            // We only output whitespace if the line already has some tokens on it (a line never starts with whitespace)
            if (_flow.Tokens.Count > 0)
            {
                foreach (Token token in _pendingWhitespace) OutputToken(token);
                _pendingWhitespace.Clear();
            }
            else AbandonPendingWhitespace();
        }

        /// <summary>
        /// Outputs currently pending text tokens.
        /// </summary>
        private void OutputPendingText()
        {
            foreach (Token token in _pendingText) OutputToken(token);
            _pendingText.Clear();
        }

        /// <summary>
        /// Outputs the specified token.
        /// </summary>
        /// <param name="token">The token</param>
        private void OutputToken(Token token)
        {
            UpdateLineHeight(token.Inline, token.FontInfo);
            _flow.Tokens.Add(token);
        }

        /// <summary>
        /// Parses the characters in the inline text's text string. The characters will be parsed into whitespace tokens or text
        /// (non-whitespace) tokens, and the tokens added to the output. As tokens spill over the engine's bounds new lines are
        /// created, until there is no more space inside the bounds.
        /// </summary>
        /// <param name="inline">The inline text to parse</param>
        /// <param name="fontInfo">The font information associated with this inline text</param>
        private void ParseCharacters(InlineText inline, FontInfo fontInfo)
        {
            // Set up state
            char ch;                // Currently read character
            float charWidth;        // The width of the current character in the active font and formatting style
            int stringFinger = 0;   // Position of the parser in the text string
            int tokenStart = 0;     // The current token's starting index in the string.

            // Parse each character in the text. If at any time we have reached the bottom of the bounding box, we stop.
            while (stringFinger < inline.Text.Length) // && _cumulativeLineHeight<=_bounds.Height)
            {
                // Read the character and calculate its width.
                ch = inline.Text[stringFinger];
                charWidth = _format.TextRatio *
                            (_format.CharacterSpacing + inline.FontSize * fontInfo.GetCharacterWidth(ch) / (float)fontInfo.UnitsPerEm);

                switch (_state)
                {
                    case ParseState.Whitespace:
                        // In the whitespace mode, we read whitespace characters. They are added to the current token.
                        // Once a non-whitespace character is read, the whitespace token is completed. It is not output until a subsequent
                        // text token is completed. The whitespace is parked instead, into a pending tokens list.
                        // If a whitespace token is completed but it doesn't fit on the current line it is abandoned. A new line is started,
                        // and all pending whitespace is abandoned.
                        if (ch < 33)
                        {
                            // If we found a newline character (\n) then we abandon the current whitespace and start a new line.
                            if ('\n' == ch)
                            {
                                AbandonPendingWhitespace();
                                NextLine(true); if (_cumulativeLineHeight > _bounds.Height) return;
                                _tokenWidth = 0;
                                tokenStart = stringFinger + 1;
                            }
                            else
                            {
                                // We found more whitespace, so we add the character metrics to the running token width.
                                // This includes word spacing, for each whitespace character.
                                _tokenWidth += charWidth + (_format.WordSpacing * _format.TextRatio);
                            }
                        }
                        else
                        {
                            // This marks the end of the whitespace token.
                            // If the whitespace fell off the end of the line we ignore it, and abandon all pending whitespace.
                            if (_lineWidth + _tokenWidth > _bounds.Width)
                            {
                                AbandonPendingWhitespace();
                                NextLine(); if (_cumulativeLineHeight > _bounds.Height) return;
                            }
                            else
                            {
                                // This whitespace fits on the line. We add the whitespace token to the line's running length but we
                                // don't output the whitespace token. We stick it in the pending tokens list.
                                _whitespaceWidth = _tokenWidth;
                                if (stringFinger > tokenStart) StorePendingWhitespace(new Token(TokenType.Whitespace, inline, fontInfo, _tokenWidth, tokenStart, stringFinger - 1));
                            }

                            // We switch to text mode
                            _state = ParseState.Text;
                            _tokenWidth = charWidth;
                            tokenStart = stringFinger;
                        }
                        break;

                    case ParseState.Text:
                        // In the text mode, we read text characters. They are added to the current token.
                        // Once a whitespace character is read, the token is completed and it is output immediately.
                        // If a text token is completed but it does not fit on the current line, a new line is started and all pending
                        // whitespace is discarded.
                        // If a text token is completed, and it fits on the current line, all pending whitespace is output as well.
                        if (ch > 32)
                        {
                            // We found more text so we add the character's width to the running token width.
                            _tokenWidth += charWidth;
                        }
                        else
                        {
                            // This marks the end of the text token.
                            // If the text fell off the end of the line, we start a new line. We discard all pending whitespace.
                            if (_lineWidth + _whitespaceWidth + _tokenWidth > _bounds.Width)
                            {
                                AbandonPendingWhitespace();
                                NextLine(); if(_cumulativeLineHeight > _bounds.Height) return;
                            }
                            else
                            {
                                // This text fits on the line. We output the pending whitespace tokens.
                                OutputPendingWhitespace();
                            }

                            // We output pending text tokens, followed by the current text token if there is one.
                            _lineWidth += _whitespaceWidth + _tokenWidth;
                            OutputPendingText();

                            if (stringFinger > tokenStart)
                                OutputToken(new Token(TokenType.Text, inline, fontInfo, _tokenWidth, tokenStart, stringFinger - 1));

                            // Reset whitespace width
                            _whitespaceWidth = 0f;

                            // If the token was a newline (\n) then we start a new line.
                            // If the token was regular whitespace we switch to whitespace mode.
                            if ('\n' == ch)
                            {
                                NextLine(true); if (_cumulativeLineHeight > _bounds.Height) return;
                                _tokenWidth = 0;
                                tokenStart = stringFinger + 1;
                            }
                            else
                            {
                                // We switch to whitespace mode
                                _state = ParseState.Whitespace;
                                _tokenWidth = charWidth + (_format.WordSpacing * _format.TextRatio);
                                tokenStart = stringFinger;
                            }
                        }
                        break;
                } // switch state

                // .. and the next token
                stringFinger++;

            } // for each character

            // We are now done with this section. We add the current token to the pending whitespace or pending text list.
            switch (_state)
            {
                case ParseState.Whitespace:
                    StorePendingWhitespace(new Token(TokenType.Whitespace, inline, fontInfo, _tokenWidth, tokenStart, stringFinger - 1)); break;
                case ParseState.Text:
                    StorePendingText(new Token(TokenType.Text, inline, fontInfo, _tokenWidth, tokenStart, stringFinger - 1)); break;
            }

        } // ParseCharacters()

        /// <summary>
        /// Stores a token in the list of pending text tokens.
        /// </summary>
        /// <param name="token">The token</param>
        private void StorePendingText(Token token)
        {
            System.Diagnostics.Debug.Assert(TokenType.Text == token.TokenType);
            _pendingText.Add(token);
        }

        /// <summary>
        /// Stores a token in the list of pending whitespace tokens.
        /// </summary>
        /// <param name="token">The token</param>
        private void StorePendingWhitespace(Token token)
        {
            System.Diagnostics.Debug.Assert(TokenType.Whitespace== token.TokenType);
            _pendingWhitespace.Add(token);
        }

        /// <summary>
        /// Updates the line height to accommodate for a larger current font size and/or descent.
        /// </summary>
        /// <param name="inline">The inline text to base the update on.</param>
        /// <param name="fontInfo">The information for the current font.</param>
        private void UpdateLineHeight(InlineText inline, FontInfo fontInfo)
        {
            _currentFontSize = inline.FontSize;

            if (inline.FontSize > _largestFontSize)
            {
                _lineHeight += inline.FontSize - _largestFontSize;
                _cumulativeLineHeight += inline.FontSize - _largestFontSize;
                _largestFontSize = inline.FontSize;
            }

            float descent = inline.FontSize * fontInfo.Descent/(float)fontInfo.UnitsPerEm;
            if ( descent < _flow.Descent) _flow.Descent = descent;
        }
        #endregion



        // Public constructor
        // ==================
        #region Public constructor
        /// <summary>
        /// Constructs a new TextFlowEngine instance.
        /// </summary>
        public TextFlowEngine()
        {
            _fontDirectory = new Dictionary<string, FontInfo>();
            _pendingText = new List<Token>();
            _pendingWhitespace = new List<Token>();
        }
        #endregion



        // Private types
        // =============
        #region Private types
        private enum ParseState
        {
            Whitespace,
            Text
        }
        #endregion
    } // TextFlowEngine class
}
